﻿Imports Microsoft.VisualStudio.DebuggerVisualizers
Imports System.ComponentModel
Imports System.Data.Common
Imports System.Data.Metadata.Edm
Imports System.Data.Objects
Imports System.Data.Objects.DataClasses
Imports System.IO
Imports System.Reflection
Imports System.Text

Public Class EntityCollectionVisualizerObjectSource
    Inherits VisualizerObjectSource

    '''' <summary>
    '''' </summary>
    '''' <param name="target"></param>
    '''' <param name="outgoingData"></param>
    '''' <remarks>Warning: detached entities will not show up!</remarks>
    Public Overrides Sub GetData(ByVal target As Object, ByVal outgoingData As Stream)
        Dim ec As IEnumerable = TryCast(target, IEnumerable)
        If (ec Is Nothing) Then
            ' the visualizer can be opened for all descendants of RelatedEnd
            Windows.Forms.MessageBox.Show("This visualizer can only show objects of type EntityCollection")
            Return
        End If

        Dim ds As New DataSet()
        ' table with query values
        ds.Tables.Add(Constants.TABLE_VALUES)
        ds.Tables(Constants.TABLE_VALUES).Columns.Add(Constants.COLUMN_STATE).DataType = GetType(String)

        ' find context from first entity in the collection
        Dim firstEntity As EntityObject = TryCast((From e In ec).FirstOrDefault, EntityObject)
        Dim context As ObjectContext = Nothing
        If (firstEntity IsNot Nothing) Then
            Try
                context = Utility.GetContextFromEntity(firstEntity)
                For Each objEntity As Object In ec
                    Dim entity As EntityObject = DirectCast(objEntity, EntityObject)
                    AddEntityToTable(ds.Tables(Constants.TABLE_VALUES), entity, context)
                Next
            Catch ex As Exception
                ds.ExtendedProperties.Add(Constants.EXT_PROP_ERROR_MESSAGE, ex.Message)
            End Try
        Else
            ' self tracking entities
            Try
                For Each objEntity As Object In ec
                    AddSelfTrackingEntityToTable(ds.Tables(Constants.TABLE_VALUES), objEntity)
                Next
            Catch ex As Exception
                ds.ExtendedProperties.Add(Constants.EXT_PROP_ERROR_MESSAGE, ex.Message)
            End Try
        End If

        ds.AcceptChanges()

        ds.WriteXml(outgoingData, XmlWriteMode.WriteSchema)
    End Sub

    Private Sub AddEntityToTable(ByVal dt As DataTable, ByVal entity As EntityObject, ByVal context As ObjectContext)
        Dim ose As ObjectStateEntry = Nothing
        'If object is Detached, then there will be no entry in the ObjectStateManager
        'new entities that are not attached will not even have an entitykey
        If (Not context.ObjectStateManager.TryGetObjectStateEntry(entity.EntityKey, ose)) Then
            Throw New Exception("Object is not currently being change tracked and no ObjectStateEntry exists.")
        End If

        ' create the datatable columns from the first entry
        Dim values As CurrentValueRecord = ose.CurrentValues

        'If Object is Added, there will be no Original values and it will throw an exception
        Dim originalValues As DbDataRecord = Nothing
        If ose.State <> EntityState.Added Then
            originalValues = ose.OriginalValues
        End If

        If (dt.Columns.Count = 1) Then
            ' get the values
            For i As Integer = 0 To values.FieldCount - 1
                'metadata provides field names
                Dim name = values.DataRecordInfo.FieldMetadata(i).FieldType.Name
                dt.Columns.Add(name) '.DataType = GetType(String)
                dt.Columns.Add(name & " changed").DataType = GetType(Boolean)
            Next
            If (System.Environment.Version.Major < 4) Then
                ' in .NET 3.5, the reference properties don't show up in the ObjectStateEntry (in 4.0, they do)
                ' therefore, we add these reference properties seperately
                For Each refPropInfo As PropertyInfo In From rpi In entity.GetType().GetProperties() _
                                                        Where rpi.PropertyType.IsSubclassOf(GetType(EntityReference))

                    Dim er As EntityReference = DirectCast(refPropInfo.GetValue(entity, Nothing), EntityReference)
                    For Each em As EdmMember In GetKeyNames(er)
                        If (Not dt.Columns.Contains(em.Name)) Then
                            dt.Columns.Add(em.Name)
                            dt.Columns.Add(em.Name & " changed").DataType = GetType(Boolean)
                        End If
                    Next
                Next
            End If
        End If

        Dim row As DataRow = dt.NewRow()
        row(Constants.COLUMN_STATE) = entity.EntityState.ToString()

        For i As Integer = 0 To values.FieldCount - 1
            row(i * 2 + 1) = values(i)
            If (ose.State = EntityState.Deleted OrElse ose.State = EntityState.Added) Then
                row(i * 2 + 2) = True
            Else
                Dim name = values.DataRecordInfo.FieldMetadata(i).FieldType.Name
                Dim changedProp As String = (From prop In ose.GetModifiedProperties _
                                             Where prop = name).FirstOrDefault
                Dim isModified As Boolean = (changedProp IsNot Nothing)
                row(i * 2 + 2) = isModified
            End If
        Next
        If (System.Environment.Version.Major < 4) Then
            ' Only in .NET 3.5
            Dim refPropInfos = (From rp In entity.GetType().GetProperties())
            For Each refPropInfo As PropertyInfo In From rpi In entity.GetType().GetProperties() _
                                                    Where rpi.PropertyType.IsSubclassOf(GetType(EntityReference))

                Dim er As EntityReference = DirectCast(refPropInfo.GetValue(entity, Nothing), EntityReference)
                Dim key As EntityKey = er.EntityKey
                If (key IsNot Nothing) Then
                    For Each keyvalue As EntityKeyMember In key.EntityKeyValues
                        Dim i As Integer = dt.Columns.IndexOf(dt.Columns(keyvalue.Key))
                        row(i) = keyvalue.Value.ToString()
                        row(i + 1) = False ' TODO: how to know if it has changed?
                    Next
                Else
                    For Each em As EdmMember In GetKeyNames(er)
                        Dim i As Integer = dt.Columns.IndexOf(dt.Columns(em.Name))
                        row(i) = String.Empty
                        row(i + 1) = False ' TODO: how to know if it has changed?
                    Next
                End If
            Next
        End If
        dt.Rows.Add(row)
    End Sub

    Private Sub AddSelfTrackingEntityToTable(ByVal dt As DataTable, ByVal entity As Object)
        Static dataMemberProps As List(Of PropertyInfo) = Nothing

        Dim state As String = Utility.GetStateFromEntity(entity)

        If (dt.Columns.Count = 1) Then
            ' create the datatable columns from the first entry
            dataMemberProps = Utility.GetPropertyInfos(entity)
            ' get the values
            For i As Integer = 0 To dataMemberProps.Count - 1

                Dim prop As PropertyInfo = dataMemberProps(i)

                'metadata provides field names
                dt.Columns.Add(prop.Name)
                dt.Columns.Add(prop.Name & " changed").DataType = GetType(Boolean)
            Next
        End If

        Dim row As DataRow = dt.NewRow()
        row(Constants.COLUMN_STATE) = state

        For i As Integer = 0 To dataMemberProps.Count - 1
            Dim value As Object = dataMemberProps(i).GetValue(entity, Nothing)
            row(i * 2 + 1) = Utility.NullableObjectToString(value)
            row(i * 2 + 2) = (state = "Deleted") OrElse (state = "Added") OrElse (state = "Modified" AndAlso Utility.IsPropertyChanged(entity, dataMemberProps(i).Name))
        Next
        dt.Rows.Add(row)
    End Sub

    Private Function GetKeyNames(ByVal er As EntityReference) As ReadOnlyMetadataCollection(Of EdmMember)
        Dim pi As PropertyInfo = er.GetType().GetProperty("ToEndMember", BindingFlags.NonPublic Or BindingFlags.Instance)
        Dim rsem As RelationshipEndMember = DirectCast(pi.GetValue(er, Nothing), RelationshipEndMember)
        Dim etb As EntityTypeBase = DirectCast(rsem.TypeUsage.EdmType, RefType).ElementType
        Return etb.KeyMembers
    End Function

End Class
